package com.google.code.diffusion;

import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.List;
import java.util.Locale;

import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

/** Switch focus to the previous input field when the user presses backspace
 *  and the field is empty. A single instance of this class services all the
 *  fields. */
class BackspaceHandler implements View.OnKeyListener {
  private final NewBankNoteActivity activity;
  
  public BackspaceHandler(NewBankNoteActivity activity) {
    this.activity = activity;
  }
  
  @Override
  public boolean onKey(View view, int key_code, KeyEvent event) {
    EditText edit_text = (EditText)view;
    // This callback is called twice on each key press, one time for key down
    // and one time for key up, so we just filter out one of them.
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
      if ((key_code == KeyEvent.KEYCODE_DEL) &&             // Backspace
          (edit_text.getText().toString().length() == 0)) { // Field is empty
        // Search for the index of the active field in the list of fields.
        int index;
        for (index = 0; index < activity.input_fields.length; index++) {
          if (edit_text.equals(activity.input_fields[index])) {
            break;
          }
        }
        // Switch to previous field, if it exists.
        if (index > 0) activity.input_fields[index - 1].requestFocus();
      }
    }
    return false;
  }
}

/** Switch focus to the next field when the user presses the next button in a
 *  serial number of short code input field. Validation is performed by the 
 *  FocusLostListener. A single instance of this class services all the
 *  fields. */
class NextHandler implements TextView.OnEditorActionListener {
  private final NewBankNoteActivity activity;
  
  public NextHandler(NewBankNoteActivity activity) {
    this.activity = activity;
  }

  @Override
  public boolean onEditorAction(TextView view, int action_id, KeyEvent event) {
    // Find the index of the active field in the list of fields.
    int index;
    for (index = 0; index < activity.input_fields.length; index++) {
      if (view.equals(activity.input_fields[index])) {
        break;
      }
    }
    // Switch to the next field, if any.
    if (index < activity.input_fields.length - 1)
      activity.input_fields[index + 1].requestFocus();
    
    return true;
  }
}

/** Validate input when an input field for the serial number or short code
 *  looses focus. A single instance is used to service all the fields. Needs
 *  to be initialized with the activity containing the fields. */
class FocusLostListener implements View.OnFocusChangeListener {
  private final NewBankNoteActivity activity; 

  public FocusLostListener(NewBankNoteActivity activity) {
    this.activity = activity;
  }
  
  @Override
  public void onFocusChange(View view, boolean has_focus) {
    if (!has_focus) { // Only fire on focus lost
      // Find the index of the field.
      int index;
      for (index = 0; index < activity.input_fields.length; index++) {
        if (view.equals(activity.input_fields[index])) {
          break;
        }
      }
      // Fire the appropriate checking function. 
      if (index < 2) activity.checkSerial();
      else           activity.checkShortCode();
    }
  }
  
}

/** Listen to text changes in the serial number of short code input fields, and
 *  handle when the appropriate amount of characters is reached. The input is
 *  validated with the check...() methods of the activity. When no errors occur,
 *  focus is handed to the next field.
 *  For performance reasons (this handler is called on each keypress) and for
 *  practical reasons, each field gets its own instance with an index
 *  representing its place in the order of fields, the number of chars that need
 *  to be reached and the activity on which the fields reside. */
class TextChangedHandler implements TextWatcher {
  private final int                 index;
  private final int                 num_chars;
  private final NewBankNoteActivity activity;
  
  public TextChangedHandler(int                 index,
                            int                 num_chars, 
                            NewBankNoteActivity activity) {
    this.activity  = activity;
    this.index     = index;
    this.num_chars = num_chars;
  }
  
  @Override
  public void afterTextChanged(Editable editable) {
    // If something has changed, the check on the field should be done again.
    if (activity.reported_fields[index]) {
      activity.reported_fields[index] = false;
      activity.input_fields[index].setTextColor(Color.BLACK);
    }
    
    if (editable.toString().length() == num_chars) {
      // Let the activity perform the proper check.
      boolean is_correct = false;
      if (index < 2)
        is_correct = activity.checkSerial();
      else
        is_correct = activity.checkShortCode();
      
      // If no (new) errors occured, move focus to the next field. 
      if ((is_correct) && (index < (activity.input_fields.length - 1)))
        activity.input_fields[index + 1].requestFocus();
    }
  }
  
  @Override
  public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count) {}
}

/** Listener for the available location providers that waits until one of them
 *  becomes accurate enough and then sets the location of the 
 *  NewBankNoteActivity and stops all location listening. It sets the country
 *  as soon as it gets a fix, but the city and postal code are only determined
 *  if the accuracy is better than 30 meters. */
class LocationSetter implements LocationListener {
  private final NewBankNoteActivity activity;
  private boolean first_guess = true;
  private boolean is_active   = true;
  
  public LocationSetter(NewBankNoteActivity activity) {
    this.activity = activity;
  }
  
  public void disable() {
    activity.loc_mgr.removeUpdates(this);
    is_active = false;  
  }
  
  public boolean isActive() { return is_active; }

  @Override
  public void onLocationChanged(Location location) {
    // When the first, inaccurate, location is obtained, the country is set.
    if (first_guess) {
      first_guess = false;
      activity.setCountry(location);
    }
    
    // If the accuracy becomes better than 30 meters, a precise location can be
    // extracted and listening can be stopped.
    if (location.getAccuracy() < 30.0) {
      disable();
      activity.setLocation(location);
    }
  }

  @Override
  public void onProviderDisabled(String provider) {}

  @Override
  public void onProviderEnabled(String provider) {}

  @Override
  public void onStatusChanged(String provider, int status, Bundle extras) {}

}

/** Activity for entering a new banknote. */
public class NewBankNoteActivity extends Activity {
  
  /** The BankNote we're currently operating on. */
  BankNote curr_banknote;

  protected LocationManager loc_mgr;
  private LocationSetter loc_setter;
  
  private Button   submit_button;
  private EditText edit_serial_letter;
  private EditText edit_serial_number;
  private EditText edit_short_letter;
  private EditText edit_short_plate;
  private EditText edit_short_row;
  private EditText edit_short_column;
  private EditText edit_comment;
  private Spinner  country_spinner;
  private EditText edit_city;
  private EditText edit_postal;
  
  /** The fields for serial number and short code are all stored in this list
   *  for various methods. */
  protected final EditText[] input_fields = new EditText[6];
  
  /** An error in the serial number and short code fields should never be
   *  reported twice, so once it has been reported, it should be marked as so in
   *  this array. It's the job of the keylistener to clear this again if any
   *  changes are made.
   *  The indices correspond to those in input_fields. */
  protected boolean[] reported_fields = {false, false, false, false, false, false};
  
  /** The main entrance method. All setup and initializations have to be done
   *  here. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.new_banknote);
    
    // Start the location acquisition.
    loc_mgr    = (LocationManager) 
      getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
    loc_setter = new LocationSetter(this);
    for (String provider_name : loc_mgr.getAllProviders()) {
      loc_mgr.requestLocationUpdates(provider_name, 1000, 0, loc_setter);
    }
    
    // Initialize the new BankNote.
    curr_banknote = new BankNote(getApplicationContext());
    
    // Invalidate the submit button.
    submit_button = (Button)findViewById(R.id.ButtonSubmit);
    submit_button.setEnabled(curr_banknote.ready());
    
    // Set the default for the amount selector to 50 (the most common banknote)
    // and set a listener.
    Spinner amount_spinner = (Spinner)findViewById(R.id.AmountSpinner);
    amount_spinner.setSelection(3);
    amount_spinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() {
      @Override
      /** If a new item is selected, set it to the banknote. */
      public void onItemSelected(AdapterView<?> parent, View view, int position,
                                 long id) {
        curr_banknote.setAmount((String)parent.getItemAtPosition(position));
        submit_button.setEnabled(curr_banknote.ready());        
      }

      /** If nothing is selected, set selection back to the banknote value, or
       *  if that is undefined, to 50. */
      @Override
      public void onNothingSelected(AdapterView<?> parent) {
        String amount;
        try {
          amount  = curr_banknote.getAmount();
        } catch (IllegalStateException e) {
          amount = "50";
        }
        for (int i = 0; i < parent.getCount(); i++) {
          if ((String)parent.getItemAtPosition(i) == amount) {
            parent.setSelection(i);
            break;
          }
        }
      }
    });
    
    // Initialize the serial number and short code input fields.
    edit_serial_letter = (EditText)findViewById(R.id.EditSerialLetter);
    edit_serial_number = (EditText)findViewById(R.id.EditSerialNumber);
    edit_short_letter  = (EditText)findViewById(R.id.EditShortLetter);
    edit_short_plate   = (EditText)findViewById(R.id.EditShortPlate);
    edit_short_row     = (EditText)findViewById(R.id.EditShortRow);
    edit_short_column  = (EditText)findViewById(R.id.EditShortColumn);
    input_fields[0] = edit_serial_letter;
    input_fields[1] = edit_serial_number;
    input_fields[2] = edit_short_letter;
    input_fields[3] = edit_short_plate;
    input_fields[4] = edit_short_row;
    input_fields[5] = edit_short_column;
    
    // Each of the serial number and short code input fields gets no less than
    // four listeners: one for the backspace button, one for the next button,
    // one for focus losing, and one for text changes. The first three share
    // an instance, the fourth one is instantiated for each field.
    BackspaceHandler back_handler         = new BackspaceHandler(this);
    NextHandler next_handler              = new NextHandler(this);
    FocusLostListener focus_lost_listener = new FocusLostListener(this);
    
    edit_serial_letter.setOnKeyListener(back_handler);
    edit_serial_letter.setOnEditorActionListener(next_handler);
    edit_serial_letter.setOnFocusChangeListener(focus_lost_listener);
    edit_serial_letter.addTextChangedListener(new TextChangedHandler(0, 1, this));
    edit_serial_number.setOnKeyListener(back_handler);
    edit_serial_number.setOnEditorActionListener(next_handler);
    edit_serial_number.setOnFocusChangeListener(focus_lost_listener);
    edit_serial_number.addTextChangedListener(new TextChangedHandler(1, 11, this));
    edit_short_letter.setOnKeyListener(back_handler);
    edit_short_letter.setOnEditorActionListener(next_handler);
    edit_short_letter.setOnFocusChangeListener(focus_lost_listener);
    edit_short_letter.addTextChangedListener(new TextChangedHandler(2, 1, this));
    edit_short_plate.setOnKeyListener(back_handler);
    edit_short_plate.setOnEditorActionListener(next_handler);
    edit_short_plate.setOnFocusChangeListener(focus_lost_listener);
    edit_short_plate.addTextChangedListener(new TextChangedHandler(3, 3, this));
    edit_short_row.setOnKeyListener(back_handler);
    edit_short_row.setOnEditorActionListener(next_handler);
    edit_short_row.setOnFocusChangeListener(focus_lost_listener);
    edit_short_row.addTextChangedListener(new TextChangedHandler(4, 1, this));
    edit_short_column.setOnKeyListener(back_handler);
    edit_short_column.setOnEditorActionListener(next_handler);
    edit_short_column.setOnFocusChangeListener(focus_lost_listener);
    edit_short_column.addTextChangedListener(new TextChangedHandler(5, 1, this));
    
    // The optional comment field.
    edit_comment = (EditText)findViewById(R.id.EditTextComment);
    edit_comment.setOnFocusChangeListener(new View.OnFocusChangeListener() {
      
      @Override
      public void onFocusChange(View view, boolean has_focus) {
        if (!has_focus) {
          final EditText edit_text = (EditText)view;
          curr_banknote.setComment(edit_text.getText().toString()); 
        }
      }
    });
    
    country_spinner = (Spinner) findViewById(R.id.SpinnerCountry);
    edit_city   = (EditText) findViewById(R.id.EditCity);
    edit_postal = (EditText) findViewById(R.id.EditPostal);
    View.OnFocusChangeListener stop_auto_location = new View.OnFocusChangeListener() {
      @Override
      public void onFocusChange(View view, boolean has_focus) {
        if (loc_setter.isActive()) {
          loc_setter.disable();
          Toast.makeText(getApplicationContext(),
                         R.string.MsgStopAutoLocation,
                         Toast.LENGTH_SHORT).show();
        }
      }
    };
    country_spinner.setOnFocusChangeListener(stop_auto_location);
    edit_city.setOnFocusChangeListener(stop_auto_location);
    edit_postal.setOnFocusChangeListener(stop_auto_location);
  } 

  /** Set the serial number of the banknote to the data in the input fields and
   *  report any errors if this fails. Errors are reported only once, so if this
   *  method passes, it doesn't mean the code is free of errors. Empty fields
   *  are excluded from the check. 
   *  
   * @return false if a new error occurs, or true if no new error occurs.
   */
  public boolean checkSerial() {
    int     wrong_field_index = 0;    // Note the field with the error
    String  error_msg         = "";   // The error message reported by the
                                      // Banknote class and displayed to the
                                      // user.
    
    try {
      curr_banknote.setSerial(edit_serial_letter.getText().toString(), 
                              edit_serial_number.getText().toString());
      submit_button.setEnabled(curr_banknote.ready());
    } catch (InvalidParameterException e) {
      error_msg  = e.getMessage();
      if (error_msg.startsWith("Expected one letter")) {
        wrong_field_index = 0;
      } else if (error_msg.startsWith("Expected 11 numbers")) {
        wrong_field_index = 1;
      } else if (error_msg.startsWith("Unknown country code")) {
        wrong_field_index = 0;
      } else if (error_msg.endsWith("is not a number")) {
        wrong_field_index = 1;
      } else if (error_msg.equals("Error in number (checksum fails)")) {
        wrong_field_index = 1;
      }
      
      // If the field hasn't been marked as an error previously, report the
      // error.
      return !markError(error_msg, wrong_field_index);
    }
    return true;
  }
  
  public boolean checkShortCode() {
    int     wrong_field_index = 0;    // Note the field with the error
    String  error_msg         = "";   // The error message reported by the
                                      // Banknote class and displayed to the
                                      // user.
    
    try {
      curr_banknote.setShortCode(edit_short_letter.getText().toString(),
                                 edit_short_plate.getText().toString(),
                                 edit_short_row.getText().toString(),
                                 edit_short_column.getText().toString());
      submit_button.setEnabled(curr_banknote.ready());
    } catch (InvalidParameterException e) {
      error_msg  = e.getMessage();
      if (error_msg.startsWith("Expected one letter for country")) {
        wrong_field_index = 2;
      } else if (error_msg.startsWith("Unknown country code")) {
        wrong_field_index = 2;
      } else if (error_msg.startsWith("Expected 3 numbers for plate")) {
        wrong_field_index = 3;
      } else if (error_msg.endsWith("is not a valid plate number")) {
        wrong_field_index = 3;
      } else if (error_msg.startsWith("Expected one letter for row")) {
        wrong_field_index = 4;
      } else if (error_msg.endsWith("is not a valid row letter")) {
        wrong_field_index = 4;
      } else if (error_msg.startsWith("Expected one number for column")) {
        wrong_field_index = 5;
      } else if (error_msg.endsWith("is not a valid column number")) {
        wrong_field_index = 5;
      }
      // If the field hasn't been marked as an error previously, report the
      // error.
      return !markError(error_msg, wrong_field_index);
    }
    return true;
  }

  /** Handle the error reporting of the specified field, including checking
   *  if it isn't already reported, shaking, coloring, notifying and marking as
   *  reported.
   *  @param error_msg is the message to be displayed to the user.
   *  @param field_index is the number of the EditText field where the error is.
   *  @return true if the error has been reported or false if it has been
   *          suppressed.
   */
  private boolean markError(String error_msg, int field_index) {
    if (!reported_fields[field_index]) {
      EditText wrong_field = input_fields[field_index];
      if (wrong_field.getText().toString().length() > 0) { // If field is not empty
        Toast.makeText(getApplicationContext(), error_msg, Toast.LENGTH_SHORT).show();
        wrong_field.setTextColor(Color.RED);
        Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake);
        wrong_field.startAnimation(shake);
        reported_fields[field_index] = true;
        return true;
      }
    }
    return false;
  }
  
  /** Set the City, Postal code and Country based on the provided Location
   *  object. It is assumed that the Location object has enough precision. If
   *  the geographic data cannot be resolved (for example because the network
   *  is unavailable), it is notified to the user.
   * @param location the Location instance with the latitude and longitude.
   */
  protected void setLocation(Location location) {
    boolean succes = true;
    
    Geocoder geocoder = new Geocoder(getApplicationContext(), Locale.ENGLISH);
    try {
      // Try to get an address from Googles reverse geocoding service.
      List<Address> addresses = geocoder.getFromLocation(location.getLatitude(),
                                                         location.getLongitude(),
                                                         1);
      
      if (addresses.size() > 0) {
        // If address was found, set it to the relevant fields.
        edit_city.setText(addresses.get(0).getLocality());
        edit_postal.setText(addresses.get(0).getPostalCode());
        // Country may have been set already, but not with an accurate location.  
        setCountry(location);
      } else {
        succes = false;
      }
    } catch (IOException e) {
      succes = false;
    }
    
    // Notify the user of failure.
    if (!succes) {
      Toast.makeText(getApplicationContext(),
                     R.string.MsgNoLocationFound,
                     Toast.LENGTH_SHORT).show();
      edit_city.setHint(R.string.HintManualLocation);
      edit_postal.setHint(R.string.HintManualLocation);
    }
  }
  
  /** Set the country spinner to the country where the provided Location object
   *  points to. Some conversion from Google reverse geocoding service data
   *  to Eurobilltracker names may be done, but this is far from complete.
   *  Failures are silently ignored. 
   * @param location the Location instance with the latitude and longitude.
   */
  protected void setCountry(Location location) {
    Geocoder geocoder = new Geocoder(getApplicationContext(), Locale.ENGLISH);
    try {
      // Try to get an address from Google reverse geocoding service.
      List<Address> addresses = geocoder.getFromLocation(location.getLatitude(),
                                                         location.getLongitude(),
                                                         1);

      if (addresses.size() > 0) {
        // Google and Eurobilltracker country names may have small differences.
        // To facilitate matching, everything is treated in uppercase and known
        // differences (currently just one :P) are fixed. 
        String curr_country = addresses.get(0).getCountryName().toUpperCase();
        if (curr_country.equals("THE NETHERLANDS")) {
          curr_country = "NETHERLANDS";
        }
        // Match the spinners values to the found country name and select it if
        // it matches.
        for (int i = 0; i < country_spinner.getCount(); i++) {
          if (country_spinner.getItemAtPosition(i).toString().toUpperCase().equals(curr_country)) {
            country_spinner.setSelection(i);
            break;
          }
        }
      }
    } catch (IOException e) { /* Pass */ }
    
  }
}
